//==================================================================
//  Copyright (C) 2006-2007  Davide Pasca
//
//  This library is free software; you can redistribute it and/or
//  modify it under the terms of the GNU Lesser General Public
//  License as published by the Free Software Foundation; either
//  version 2.1 of the License, or (at your option) any later version.
//
//  This library is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//  Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public
//  License along with this library; if not, write to the Free Software
//  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
//==================================================================
///
///
///
///
///
//==================================================================

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <gl/glew.h>

#include "psys.h"
#include "console.h"
#include "font.h"
#include "gfxutils.h"

//==================================================================
Console::Console( Window *parent_winp, void *userdatap ) :
	_edit_cursor_pos(0),
	_userdatap(0),
	_linecb(0),
	_n_lines(0),
	_first_line_idx(0),
	_n_hist_lines(0),
	_first_hist_line_idx(0),
	_cur_hist_line(0),
	_n_commands(0),
	_fontp(0)
{
	_edit_line_txt[0] = 0;

	win_init_quick( &_win, _T("console"), parent_winp,
		  this, (WindowEvent::Callback)eventfilter_s,
		  Window::ANCH_TYPE_PARENT_X1,	0,
		  Window::ANCH_TYPE_PARENT_Y2,	-160,
		  Window::ANCH_TYPE_PARENT_X2,	0,
		  Window::ANCH_TYPE_PARENT_Y2,  0,
		  WIN_INIT_FLG_OPENGL );

	_userdatap = userdatap;

	_fontp = &WinSys::GetFont();
}

//==================================================================
Console::~Console()
{
}

//==================================================================
int Console::cons_parent_eventfilter( void *userobjp, WindowEvent *eventp )
{
	switch ( eventp->GetType() )
	{
	case WindowEvent::ETYPE_KEYDOWN:
		if ( eventp->keycode == VK_ESCAPE )
		{
			if ( IsShowing() )
				Show( 0 );
		}
		break;

	case WindowEvent::ETYPE_KEYCHAR:
		if ( eventp->keychar == '/' )	// `/~
			if ( !IsShowing() )
			{
				Show( 1 );
				if NOT( cons_editline_get()[0] )
					cons_editline_set( _T("/") );
			}
		break;
	}

	return 0;
}

//==================================================================
void Console::draw_edit_line()
{
	TCHAR	buff_cut[CONS_LINE_MAX_CHARS + 32];
	TCHAR	buff[CONS_LINE_MAX_CHARS + 32];
	int		x_cur;
	int		h_cur;

	_tcsncpy_s( buff_cut, _edit_line_txt, _edit_cursor_pos );
	buff_cut[_edit_cursor_pos] = 0;

	_stprintf_s( buff, _T("> %s"), buff_cut );

	_fontp->DrawBegin();

		x_cur = _fontp->StringWidth( buff );
		h_cur = _fontp->TextHeight() + 2;

		glColor3f( 0.6f, 0.6f, 1 );

		_stprintf_s( buff, _T("> %s"), _edit_line_txt );
		_fontp->puts( buff );

	_fontp->DrawEnd();

	glDisable( GL_TEXTURE_2D );
	glColor3f( 0, 1, 0 );
	glBegin( GL_QUADS );
		glVertex2f( x_cur, -1 );			glVertex2f( x_cur + 2, -1 );
		glVertex2f( x_cur + 2, h_cur+1 );	glVertex2f( x_cur, h_cur+1 );
	glEnd();
}

//==================================================================
void Console::do_paint()
{
	int			i, line_idx;
	int			line_he, y;
	TextLine	*linep;

	glClearColor( 0, 0, 0.2f, 1 );
	glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

	begin_2D( 0, 0 );

		line_he = _fontp->TextHeight();

		glPushMatrix();
	
			y = _win.GetHeight() - line_he;
			glTranslatef( 0, y, 0 );
			draw_edit_line();

			_fontp->DrawBegin();
			for (i=_n_lines-1; i >= 0; --i)
			{
				line_idx = (_first_line_idx + i) % CONS_MAX_LINES;
				linep = _lines[ line_idx ];

				y += -line_he;
				glTranslatef( 0, -line_he, 0 );


				if ( linep->flags & CONS_LINE_FLG_USER )
					glColor3f( 0.6f, 0.6f, 1 );
				else
					glColor3f( 1, 1, 1 );

				_fontp->printf( _T("  %s"), linep->txt );

				// don't draw lines that are not visible
				if ( y <= 0 )
					break;
			}
			_fontp->DrawEnd();

		glPopMatrix();


	end_2D();
}

//==================================================================
static void *cparr_item_add( void *items_arrayp[], int *first_item_idxp, int *n_itemsp,
							 int max_items,
							 int item_size )
{
	void	*new_itemp;

	if ( *n_itemsp < max_items )
	{
		if ( new_itemp = PSYS_MALLOC( item_size ) )
		{
			items_arrayp[ *n_itemsp ] = new_itemp;
			*n_itemsp += 1;
		}
	}
	else
	{
		SAFE_FREE( items_arrayp[ *first_item_idxp ] );
		if ( new_itemp = PSYS_MALLOC( item_size ) )
		{
			items_arrayp[ *first_item_idxp ] = new_itemp;
		}
		*first_item_idxp = (*first_item_idxp + 1) % max_items;
	}

	return new_itemp;
}

//==================================================================
void Console::_cons_line_puts( const TCHAR *txtp, int flags )
{
	TextLine	*linep;

	if ( linep = (TextLine *)cparr_item_add( (void **)_lines, &_first_line_idx,
						&_n_lines,
						CONS_MAX_LINES,
						sizeof(*linep) ) )
	{
		_tcscpy_s( linep->txt, _countof(linep->txt), txtp );
		linep->flags = flags;

		_win.Invalidate();
	}
	else
	{
		PASSERT( 0 );
	}
}

//==================================================================
void Console::hist_line_add( const TCHAR *txtp )
{
	TCHAR	*line_txtp;

	if ( txtp[0] == 0 )	// if len == 0
		return;

	int txt_len = _tcslen(txtp);
	int	item_size = (txt_len+1) * sizeof(TCHAR);


	if ( line_txtp = (TCHAR *)cparr_item_add( (void **)_hist_lines_ap,
								&_first_hist_line_idx,
								&_n_hist_lines,
								CONS_HIST_MAX_LINES,
								item_size ) )
	{
		_tcscpy_s( line_txtp, txt_len+1, txtp );
	}
	else
	{
		PASSERT( 0 );
	}

	_cur_hist_line = -1;
}

//==================================================================
void Console::PutLine( const TCHAR *txtp )
{
	_cons_line_puts( txtp, 0 );
}

//==================================================================
void Console::cons_line_vprintf( const TCHAR *strp, va_list valist )
{
	TCHAR	buff[4096];
	int		len;

	len = _vstprintf_s( buff, strp, valist );
	PASSERT( len >= 0 );
	if ( len > 0 )
	{
		PutLine( buff );
	}
}

//==================================================================
void Console::LinePrintF( const TCHAR *strp, ... )
{
	va_list	va;

	va_start( va, strp );
		cons_line_vprintf( strp, va );
	va_end( va );
}

//==================================================================
static bool is_control_char( int keychar )
{
	return keychar <= 31;
}

//==================================================================
int Console::find_cmd_idx( const TCHAR *cmdnamep )
{
	for (int i=0; i < _n_commands; ++i)
	{
		if ( !_tcsicmp( _cmd_items[i].name, cmdnamep ) )
			return i;
	}

	return -1;
}

//==================================================================
static TCHAR *find_first_sep( TCHAR *txtp, TCHAR *separatorsp )
{
	int	nsep = _tcslen( separatorsp );

	while ( txtp[0] )
	{
		for (int i=0; i < nsep; ++i)
			if ( txtp[0] == separatorsp[i] )
				return txtp;

		++txtp;
	}

	return txtp;
}

//==================================================================
static TCHAR *find_first_nonsep( TCHAR *txtp, TCHAR *separatorsp )
{
	int	nsep = _tcslen( separatorsp );
	int	i;

	while ( txtp[0] )
	{
		for (i=0; i < nsep; ++i)
			if ( txtp[0] == separatorsp[i] )
				break;

		if ( i == nsep )
			return txtp;

		++txtp;
	}

	return txtp;
}

//==================================================================
static int split_line( TCHAR *linep, TCHAR *pointers[128], TCHAR *separatorsp, int sep_generates_empty_string )
{
	TCHAR	*linep2, *tokenp_end, *tokenp;
	int		n_ptrs;

	n_ptrs = 0;
	linep2 = linep;
	while ( linep2[0] )
	{
		tokenp = find_first_nonsep( linep2, separatorsp );
		if ( sep_generates_empty_string )
		{
		TCHAR	*p;

			for (p=linep2; p < tokenp; ++p)
			{
				PASSERT( n_ptrs < 128 );
				if ( n_ptrs >= 128 )
					return -1;

				p[0] = 0;					// close string onto itself (0)
				pointers[ n_ptrs++ ] = p;	// assign string to pointer
			}
		}

		tokenp_end = find_first_sep( tokenp, separatorsp );
		if ( tokenp_end[0] == 0 )
		{
			PASSERT( n_ptrs < 128 );
			if ( n_ptrs >= 128 )
				return -1;

			pointers[ n_ptrs++ ] = tokenp;
			break;
		}
		else
		{
			pointers[ n_ptrs++ ] = tokenp;
			tokenp_end[0] = 0;
			linep2 = tokenp_end;
		}

		++linep2;
	}

	return n_ptrs;
}

//==================================================================
void Console::execCmd( const TCHAR *cmdlinep )
{
	TCHAR		buff[CONS_LINE_MAX_CHARS];
	TCHAR		*split_ptrs[128];
	int			n_parts;
	int			cmd_idx;

	_tcscpy_s( buff, cmdlinep );		// save buffer for splitting

	n_parts = split_line( buff, split_ptrs, _T(" "), 0 );
	PASSERT( n_parts > 0 );
	if ( n_parts <= 0 )
		return;

	//---- check if the command is help, in which case we handle it !
	if NOT( _tcsicmp( _T("/help"), split_ptrs[0] ) )
	{
	int	i;

		PutLine( _T("----------------------------") );
		for (i=0; i < _n_commands; ++i)
		{
			if ( _cmd_items[i].helplinep )
				PutLine( _cmd_items[i].helplinep );
		}
		PutLine( _T("----------------------------") );
	}
	else	// otherwise proceed to look for user commands
	{
		if ( (cmd_idx = find_cmd_idx( split_ptrs[0] )) < 0 )
		{
			if ( _linecb )
				_linecb( _userdatap, cmdlinep, 0 );

			return;
		}

		if ( _cmd_items[cmd_idx].callback )
			_cmd_items[cmd_idx].callback( //_cmd_items[cmd_idx].userdatap,
											_userdatap,
											&split_ptrs[1], n_parts - 1 );

		if ( _linecb )
			_linecb( _userdatap, cmdlinep, 1 );
	}
}

//==================================================================
void Console::cons_cmd_execute_printf( const TCHAR *cmdfmtp, ... )
{
	TCHAR	buff[4096];
	int		len;
	va_list	va;

	va_start( va, cmdfmtp );
	len = _vstprintf_s( buff, cmdfmtp, va );
	va_end( va );

	PASSERT( len >= 0 );
	if ( len > 0 )
	{
		execCmd( buff );
	}
}

//==================================================================
void Console::handle_new_line()
{
	int		len;

	if NOT( len = _tcslen( _edit_line_txt ) )
		return;

	_cons_line_puts( _edit_line_txt, CONS_LINE_FLG_USER );

	hist_line_add( _edit_line_txt );

	execCmd( _edit_line_txt );

	_edit_cursor_pos = 0;
	_edit_line_txt[0] = 0;
}

//==================================================================
void Console::handle_keydown( WindowEvent *eventp )
{
int		pos, len;

	switch ( eventp->keycode )
	{
	case VK_ESCAPE:
		Show( 0 );
		break;

	case VK_LEFT:
		if ( _edit_cursor_pos > 0 )
			_edit_cursor_pos -= 1;
		break;

	case VK_BACK:
		if ( _edit_cursor_pos > 0 )
		{
			pos = _edit_cursor_pos;
			len = _tcslen( _edit_line_txt );
			memmove( _edit_line_txt + pos - 1, _edit_line_txt + pos, (len - pos + 1) * sizeof(TCHAR) );	// + 1 includes 0 termination

			_edit_cursor_pos -= 1;
		}
		break;

	case VK_RIGHT:
		len = _tcslen(_edit_line_txt);
		if ( _edit_cursor_pos < len )
			_edit_cursor_pos += 1;
		break;

	case VK_DELETE:
		len = _tcslen( _edit_line_txt );
		if ( _edit_cursor_pos < len )
		{
			pos = _edit_cursor_pos;
			memmove( _edit_line_txt + pos, _edit_line_txt + pos + 1, (len - pos + 1) * sizeof(TCHAR) );	// + 1 includes 0 termination
		}
		break;

	case VK_UP:
		if ( _n_hist_lines )
		{
			if ( _cur_hist_line == -1 )
				_cur_hist_line = _n_hist_lines - 1;
			else
			{
				_cur_hist_line -= 1;
				if ( _cur_hist_line < 0 )
					_cur_hist_line += _n_hist_lines;
			}
			_cur_hist_line += _first_hist_line_idx;
			_cur_hist_line %= _n_hist_lines;
			PASSERT( _cur_hist_line >= 0 && _cur_hist_line < _n_hist_lines );

			_tcscpy_s( _edit_line_txt, _hist_lines_ap[ _cur_hist_line ] );
			_edit_cursor_pos = _tcslen(_edit_line_txt);
		}
		break;

	case VK_DOWN:
		if ( _n_hist_lines && _cur_hist_line != -1 )
		{
			_cur_hist_line += 1;
			_cur_hist_line += _first_hist_line_idx;
			_cur_hist_line %= _n_hist_lines;
			PASSERT( _cur_hist_line >= 0 && _cur_hist_line < _n_hist_lines );

			_tcscpy_s( _edit_line_txt, _hist_lines_ap[ _cur_hist_line ] );
			_edit_cursor_pos = _tcslen(_edit_line_txt);
		}
		break;
	}
}

//==================================================================
int Console::eventfilter_s( Console *mythis, WindowEvent *eventp )
{
	return mythis->eventfilter( eventp );
}

//==================================================================
int Console::eventfilter( WindowEvent *eventp )
{
	switch ( eventp->GetType() )
	{
	case WindowEvent::ETYPE_LBUTTONDOWN:
		break;

	case WindowEvent::ETYPE_MOUSEMOVE:
		break;

	case WindowEvent::ETYPE_LBUTTONUP:
		break;

	case WindowEvent::ETYPE_KEYDOWN:
		handle_keydown( eventp );
		eventp->winp->Invalidate();
		break;

	case WindowEvent::ETYPE_KEYCHAR:
		{
			int		pos, len;
			TCHAR	key;

			key = eventp->keychar;//tolower( eventp->keycode );
			if ( eventp->keychar == _TXCHAR('`') || eventp->keychar == _TXCHAR('~') )
			{
				//Show( !IsShowing() );
				//Show( 0 );
			}
			else
			if ( eventp->keychar == 0x0D )
			{
				handle_new_line();
				eventp->winp->Invalidate();
			}
			else
			if NOT( is_control_char( key ) )
			{
				pos = _edit_cursor_pos;
				len = _tcslen( _edit_line_txt );
				if ( _edit_cursor_pos < len )
				{
					memmove( _edit_line_txt + pos + 1, _edit_line_txt + pos, (len - pos + 1) * sizeof(TCHAR) );	// + 1 includes 0 termination
				}

				if ( len < CONS_LINE_MAX_CHARS )
				{
					_edit_line_txt[ pos ] = key;
					_edit_line_txt[ len + 1 ] = 0;

					_edit_cursor_pos += 1;
				}
			}
		}
		break;

	case WindowEvent::ETYPE_PAINT:
		do_paint();
		break;
	}

	return 0;
}

//==================================================================
void Console::SetLineCallback( LineCallback linecb )
{
	_linecb = linecb;
}

//==================================================================
int Console::IsShowing()
{
	return _win.IsShowing();
}

//==================================================================
void Console::Show( int onoff )
{
	_win.Show( onoff );
	if ( onoff )
		_win.SetFocus();
}

//==================================================================
void Console::cons_setfocus()
{
	_win.SetFocus();
}

//==================================================================
void Console::AddCommand( TCHAR *namep, cons_cmd_cb callback, const TCHAR *helplinep )
{
console_cmd_item_t	*itemp;

	PASSERT( _n_commands < CONS_MAX_COMMANDS );
	if NOT( _n_commands < CONS_MAX_COMMANDS )
		return;

	itemp = &_cmd_items[ _n_commands++ ];

	_tcscpy_s( itemp->name, namep );
	itemp->callback	 = callback;
	itemp->helplinep = helplinep;
}

//==================================================================
void Console::AddCommandDefs( cons_cmd_def_t definitions[] )
{
	for (int i=0; definitions[i].namep; ++i)
		AddCommand( definitions[i].namep, definitions[i].callback, definitions[i].helplinep );
}

//==================================================================
TCHAR *Console::cons_editline_get()
{
	return _edit_line_txt;
}

//==================================================================
void Console::cons_editline_set( const TCHAR *editlinep )
{
	_tcscpy_s( _edit_line_txt, editlinep );
	_edit_cursor_pos = _tcslen(_edit_line_txt);
}
